Skip to content

Detect and warn about mutual recursion with #[kani::recursion]#4580

Open
feliperodri wants to merge 1 commit intomodel-checking:mainfrom
feliperodri:warn-mutual-recursion-contracts
Open

Detect and warn about mutual recursion with #[kani::recursion]#4580
feliperodri wants to merge 1 commit intomodel-checking:mainfrom
feliperodri:warn-mutual-recursion-contracts

Conversation

@feliperodri
Copy link
Copy Markdown
Contributor

Emit a compile-time warning when a function with #[kani::recursion] is involved in mutual recursion. The per-function REENTRY mechanism only handles direct recursion soundly and mutual recursion silently produces unsound results with no diagnostic.

Contributes to #3316, #3273.

Problem

The #[kani::recursion] attribute uses a per-function REENTRY flag to detect when a function calls itself, replacing the recursive call with the function's contract. For mutual recursion (f calls g, g calls f), the REENTRY flag for g is never set when verifying f's contract, so g's body executes fully instead of being replaced by its contract. This is a silent soundness gap — verification succeeds but the result is meaningless.

Solution

  • check_mutual_recursion() in contracts.rs: When a function with #[kani::recursion] is being processed in RecursiveCheck mode, scans its MIR body for calls to other functions that have contracts. For each such callee, checks if the callee's body calls back to the original function (one level of indirection). If so, emits a span_warn at the call site.
  • Only one warning per function to avoid noise.
  • Only checks one level of indirection (f→g→f). Deeper chains (f→g→h→f) are not detected — this is a known limitation documented in the warning.

Example warning

warning: `#[kani::recursion]` is used on `mutual_a`, which calls `mutual_b`
         that calls back to `mutual_a`. Mutual recursion is not supported by
         contract verification and may produce unsound results. Only direct
         recursion (a function calling itself) is handled correctly.
  --> tests/expected/function-contract/mutual_recursion_unsound.rs:12:30

Testing

  • mutual_recursion_unsound.rs — two mutually recursive functions (mutual_amutual_b) with contracts and #[kani::recursion], verifying the warning is emitted.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@feliperodri feliperodri added this to the Contracts milestone Apr 19, 2026
@feliperodri feliperodri added [C] Feature / Enhancement A new feature request or enhancement to an existing feature. [F] Soundness Kani failed to detect an issue Z-Contracts Issue related to code contracts labels Apr 19, 2026
@github-actions github-actions bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Apr 19, 2026
@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch from 8ee1f22 to 1c1e669 Compare April 19, 2026 19:01
…recursion]

The per-function REENTRY mechanism used by #[kani::recursion] only handles
direct recursion (f calls f) soundly. For mutual recursion (f calls g,
g calls f), the REENTRY flag for g is never set, so g's body executes
fully instead of being replaced by its contract. This is a silent
soundness gap — no error or warning was emitted.

This change adds check_mutual_recursion() in the contract transform pass.
When a function with #[kani::recursion] is being processed in
RecursiveCheck mode, we scan its MIR body for calls to other functions
that also have contracts AND #[kani::recursion]. For each such callee,
we check if the callee's body calls back to the original function. If
so, we emit a span_warn pointing at the call site.

We require both has_contract() and has_recursion() on the callee because
if the callee has a contract but no #[kani::recursion], Kani replaces
the call with the contract abstraction — no mutual recursion occurs.

Limitations:
- Only detects one level of indirection (f->g->f), not deeper chains.
- Reports only the first mutual-recursive callee per function.

Includes a test case (mutual_recursion_unsound.rs) with two mutually
recursive functions that triggers the warning.
@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch from 1c1e669 to 5b1c860 Compare April 19, 2026 19:06
@feliperodri feliperodri marked this pull request as ready for review April 19, 2026 19:07
@feliperodri feliperodri requested a review from a team as a code owner April 19, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[C] Feature / Enhancement A new feature request or enhancement to an existing feature. [F] Soundness Kani failed to detect an issue Z-CompilerBenchCI Tag a PR to run benchmark CI Z-Contracts Issue related to code contracts Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant